/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 **************************************************************************
 * MIFSS  - content storage system
 * 
 *
 * @uthors: uros.kristan@gmail.com (Uroš Kristan ) Urosk.NET
 *         jernej.svigelj@gmail.com (Jernej Švigelj) 
 */
package net.monare.mifss.workers;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.DomDriver;
import net.monare.mifss.configurations.pojo.*;
import net.monare.mifss.exceptions.ConfigurationHandlerException;
import net.monare.mifss.lib.AppContext;
import net.monare.mifss.lib.CdataWriter;
import net.monare.mifss.lib.UtilHelper;
import net.monare.mifss.workers.interfaces.ConfigurationHandler;
import net.monare.mifss.workers.interfaces.ContentPoolHandler;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import java.io.*;
import java.util.*;

@Component
public class ConfigurationHandlerImpl implements ConfigurationHandler {

    public static String XML_ENCODING = "utf-8";

    public static String[] CONF_EXTENSIONS = {"xml"};

    static Logger logger = Logger.getLogger(ConfigurationHandlerImpl.class);

    @Autowired
    public ApplicationContext applicationContext;

    @Value("${storage.configurations.directory}")
    private String storageConfigurationDirectory;

    private Map<String, StorageDef> storageDefRegistry = new LinkedHashMap<>();

    private Map<String, StorageSetDef> storageSetDefRegistry = new LinkedHashMap<>();

    private UtilHelper utilHelper = new UtilHelper();

    private File getStorageSetConfFolder() throws ConfigurationHandlerException {
        return new File(storageConfigurationDirectory);
    }

    /**
     * Loads storages configurations from strages folder. Storagesets and storagedfs are populated in internal registry
     * for faster access.
     *
     * @throws ConfigurationHandlerException
     */
    private void loadStorageSetsConfiguration() throws ConfigurationHandlerException {

        File storageSetFolder = getStorageSetConfFolder();

        logger.info("Loading storage definitions from " + storageSetFolder.getAbsolutePath());

        clearRegisters();

        // load all xml files from given folder
        for (File storageSetConFile : FileUtils.listFiles(storageSetFolder, CONF_EXTENSIONS, false)) {

            File resourceFile = null;
            InputStream is = null;
            XStream xStream = getXStream();

            logger.info("Loading storages form storages set: " + storageSetConFile);

            try {

                is = new BufferedInputStream(new FileInputStream(storageSetConFile));
                Reader reader = new InputStreamReader(is, XML_ENCODING);

                // read
                StorageSetDef storageSetDef = (StorageSetDef) xStream.fromXML(reader);

                logger.info("Loaded storage definitions: " + storageSetDef);

                addStorageSetDefToRegistry(storageSetDef);

            } catch (FileNotFoundException e) {

                String errMessage = "File not found: " + resourceFile;
                logger.error(errMessage, e);
                throw new ConfigurationHandlerException(errMessage, e);

            } catch (IOException e) {

                String errMessage = "Error occurred while working with stream to configuration resource.";
                logger.error(errMessage, e);
                throw new ConfigurationHandlerException(errMessage, e);

            } finally {

                utilHelper.closeInputStream(is);

            }

        }

    }

    private XStream getXStream() {

        XStream xStream = new XStream(new DomDriver("UTF-8") {
            public HierarchicalStreamWriter createWriter(Writer out) {
                return new CdataWriter(out);
            }
        });

        xStream.processAnnotations(StorageSetDef.class);
        xStream.processAnnotations(StorageDef.class);
        xStream.processAnnotations(ContentPoolDef.class);
        xStream.processAnnotations(Property.class);

        return xStream;

    }

    private void clearRegisters() {

        storageDefRegistry.clear();
        storageSetDefRegistry.clear();

    }

    private void addStorageToRegistry(StorageDef storageDef) {

        storageDefRegistry.put(storageDef.getName(), storageDef);

    }

    private void removeStorageDefFromRegistry(String storageDefName) {

        storageDefRegistry.remove(storageDefName);

    }

    private void addStorageSetDefToRegistry(StorageSetDef storageSetDef) {

        storageSetDefRegistry.put(storageSetDef.getName(), storageSetDef);

        for (StorageDef storageDef : storageSetDef.getStorageDefList()) {

            storageDef.setStorageSetDef(storageSetDef);
            addStorageToRegistry(storageDef);
        }

    }

    private void removeStorageSetDefFromRegistry(String storageSetDefName) {

        StorageSetDef storageSetDef = storageSetDefRegistry.get(storageSetDefName);

        storageSetDefRegistry.remove(storageSetDefName);

        for (StorageDef storageDef : storageSetDef.getStorageDefList())
            removeStorageDefFromRegistry(storageDef.getName());

    }

    @Override
    public void initialize() throws ConfigurationHandlerException {

        loadStorageSetsConfiguration();

    }

    @Override
    public StorageSetDef getStorageSetDefinitionFromName(String storageSetName) {

        if (storageSetDefRegistry.containsKey(storageSetName)) return storageSetDefRegistry.get(storageSetName);

        return null;

    }

    @Override
    public StorageDef getStorageDefinitionFromName(String storageName) {

        if (storageDefRegistry.containsKey(storageName)) return storageDefRegistry.get(storageName);
        return null;
    }

    @Override
    public List<String> listStorageNames() {

        List<String> resultList = new ArrayList<>();

        resultList.addAll(storageDefRegistry.keySet());

        return resultList;

    }

    @Override
    public List<StorageDef> listStorageDefinitions() {

        List<StorageDef> list = new ArrayList<>();

        list.addAll(storageDefRegistry.values());

        return list;

    }

    @Override
    public List<String> listAvailableTransformations() {

        String[] list = AppContext.getApplicationContext().getBeanNamesForType(BaseTransformationHandler.class);

        return Arrays.asList(list);

    }

    @Override
    public List<String> listStorageSetNames() throws ConfigurationHandlerException {

        List<String> resultList = new ArrayList<>();

        resultList.addAll(storageSetDefRegistry.keySet());

        return resultList;
    }

    @Override
    public List<StorageSetDef> listStorageSets() throws ConfigurationHandlerException {

        List<StorageSetDef> list = new ArrayList<>();

        list.addAll(storageSetDefRegistry.values());

        return list;

    }

    @Override
    public List<StorageDef> listStorageDefsForStorageSetDef(String storageSetName) throws ConfigurationHandlerException {

        List<StorageDef> list = new ArrayList<>();

        StorageSetDef storageSetDef = getStorageSetDefinitionFromName(storageSetName);

        for (StorageDef storageDef : storageSetDef.getStorageDefList())
            list.add(storageDef);

        return list;
    }

    @Override
    public boolean storageDefinitionExistsInStorageSetDefinition(String storageSetDefName, String storageDefName) {

        StorageSetDef storageSetDef = getStorageSetDefinitionFromName(storageSetDefName);

        StorageDef storageDef = getStorageDefinitionFromName(storageDefName);

        return (storageSetDef.getStorageDefList().contains(storageDef));
    }

    @Override
    public boolean storageDefinitionExists(String storageDefName) {
        return storageDefRegistry.containsKey(storageDefName);

    }

    @Override
    public boolean storageSetDefinitionExists(String storageSetDefName) {

        return storageSetDefRegistry.containsKey(storageSetDefName);

    }

    @Override
    public boolean setLockToStorageSetDefinition(String storageSetName, boolean locked, String lockedBy) throws ConfigurationHandlerException {

        StorageSetDef storageSetDef = getStorageSetDefinitionFromName(storageSetName);

        //TODO CHECK LOCKING USER ETC
        storageSetDef.setLocked(locked);
        // user name or some other id for locking
        storageSetDef.setLockedBy(lockedBy);
        // write update
        writeStorageSetDefinitionToFile(storageSetDef, false);

        logger.info("lock is set on storage set Definition " + storageSetName + " locked: " + locked + " by " + lockedBy);
        return true;
    }

    @Override
    public boolean addStorageSetDefinition(StorageSetDef newStorageSetDef) throws ConfigurationHandlerException {

        if(newStorageSetDef == null){
            throw new ConfigurationHandlerException("Passed storage set definition is null.");
        }

        writeStorageSetDefinitionToFile(newStorageSetDef, true);

        addStorageSetDefToRegistry(newStorageSetDef);

        return true;
    }

    @Override
    public boolean modifyStorageSetDefinition(StorageSetDef newStorageSetDef) throws ConfigurationHandlerException {

        writeStorageSetDefinitionToFile(newStorageSetDef, false);

        // remove old defintion by name
        removeStorageSetDefFromRegistry(newStorageSetDef.getName());

        //add new
        addStorageSetDefinition(newStorageSetDef);

        return true;
    }

    @Override
    public boolean removeStorageSetDefinition(String storageSetName) throws ConfigurationHandlerException {

        if (getStorageSetDefinitionFromName(storageSetName) == null) {
            String error = "Storage Set defintion with name " + storageSetName + " doesnt exist!! ";
            throw new ConfigurationHandlerException(error);
        }

        File resourceFile = new File(calculateConfigPath(storageSetName));

        boolean res = resourceFile.delete();
        if (!res) {
            logger.error("Storage set file could not be deleted from file system! " + resourceFile.getAbsolutePath());
        }

        removeStorageSetDefFromRegistry(storageSetName);

        return true;
    }

    @Override
    public void moveStorageDefinitionToStorageSetDefinition(String storageSetName, String storageName) throws ConfigurationHandlerException {

        StorageDef storageDef = getStorageDefinitionFromName(storageName);

        StorageSetDef oldStorageSet = storageDef.getStorageSetDef();

        StorageSetDef targetStorageSet = getStorageSetDefinitionFromName(storageSetName);

        if (!oldStorageSet.isLocked()) {
            String errMessage = "Storage set with name:" + storageSetName + " is not locked.";
            logger.error(errMessage);
            throw new ConfigurationHandlerException(errMessage);
        }

        if (!targetStorageSet.isLocked()) {
            String errMessage = "Storage set with name:" + storageSetName + " is not locked.";
            logger.error(errMessage);
            throw new ConfigurationHandlerException(errMessage);
        }

        // perform object move
        targetStorageSet.getStorageDefList().add(storageDef);
        oldStorageSet.getStorageDefList().remove(storageDef);

        // write new configurations
        writeStorageSetDefinitionToFile(targetStorageSet, false);
        writeStorageSetDefinitionToFile(oldStorageSet, false);

        // remove both from registry
        removeStorageSetDefFromRegistry(targetStorageSet.getName());
        removeStorageSetDefFromRegistry(oldStorageSet.getName());

        //re add them in registry
        addStorageSetDefToRegistry(targetStorageSet);
        addStorageSetDefToRegistry(oldStorageSet);

    }

    @Override
    public boolean addStorageDefinitionToStorageSetDefinition(String storageSetName, StorageDef newStorage) throws ConfigurationHandlerException {

        StorageSetDef storageSetDef = getStorageSetDefinitionFromName(storageSetName);

        storageSetDef.addStorageDef(newStorage);

        addStorageToRegistry(newStorage);

        writeStorageSetDefinitionToFile(storageSetDef, false);

        return true;
    }

    @Override
    public void modifyStorageDefinition(String storageSetName, StorageDef storageDef) throws ConfigurationHandlerException {

        StorageSetDef storageSetDef = getStorageSetDefinitionFromName(storageSetName);

        storageSetDef.removeStorageDef(storageDef);

        storageSetDef.getStorageDefList().add(storageDef);

        writeStorageSetDefinitionToFile(storageSetDef, false);

        removeStorageSetDefFromRegistry(storageSetName);

        addStorageSetDefToRegistry(storageSetDef);

    }

    @Override
    public void removeStorageDefinition(String storageSetName, String storageDefName) throws ConfigurationHandlerException {

        StorageSetDef storageSetDef = getStorageSetDefinitionFromName(storageSetName);

        StorageDef storageDefToBeRemoved = getStorageDefinitionFromName(storageDefName);

        removeStorageSetDefFromRegistry(storageSetName);

        storageSetDef.removeStorageDef(storageDefToBeRemoved);

        writeStorageSetDefinitionToFile(storageSetDef, false);

        addStorageSetDefToRegistry(storageSetDef);

    }

    @Override
    public void addApiKeyToStorageDefinition(String storageDefName, ApiKey apiKey) throws ConfigurationHandlerException {

        StorageDef storageDef = getStorageDefinitionFromName(storageDefName);

        StorageSetDef storageSetDef = getStorageSetDefinitionFromName(storageDef.getStorageSetDef().getName());

        storageDef.getApiKeys().add(apiKey);

        writeStorageSetDefinitionToFile(storageSetDef, false);

    }

    @Override
    public void modifyApiKey(String storageDefName, ApiKey modifiedApiKey) throws ConfigurationHandlerException {

        StorageDef storageDef = getStorageDefinitionFromName(storageDefName);

        StorageSetDef storageSetDef = getStorageSetDefinitionFromName(storageDef.getStorageSetDef().getName());


        for(ApiKey apiKey: storageDef.getApiKeys()){

            if(apiKey.equals(modifiedApiKey)){

                apiKey.setDescription(modifiedApiKey.getDescription());
                apiKey.setApiMode(modifiedApiKey.getApiMode());

            }
        }

        writeStorageSetDefinitionToFile(storageSetDef, false);

    }

    @Override
    public void removeApiKey(String storageDefName, ApiKey apiKeyToRemove) throws ConfigurationHandlerException {

        StorageDef storageDef = getStorageDefinitionFromName(storageDefName);

        StorageSetDef storageSetDef = getStorageSetDefinitionFromName(storageDef.getStorageSetDef().getName());
        storageDef.getApiKeys().remove(apiKeyToRemove);

        writeStorageSetDefinitionToFile(storageSetDef, false);
    }

    @Override
    public void addContentPoolDefinition(String storageName, ContentPoolDef contentPoolDef) throws ConfigurationHandlerException {

        StorageDef storageDef = getStorageDefinitionFromName(storageName);

        storageDef.getContentPoolDefs().add(contentPoolDef);

        StorageSetDef storageSetDef = storageDef.getStorageSetDef();

        writeStorageSetDefinitionToFile(storageSetDef, false);

    }

    @Override
    public void modifyContentPoolDefinition(String storageName, ContentPoolDef contentPoolDef) throws ConfigurationHandlerException {

        StorageDef storageDef = getStorageDefinitionFromName(storageName);

        storageDef.getContentPoolDefs().add(contentPoolDef);

        StorageSetDef storageSetDef = storageDef.getStorageSetDef();

        writeStorageSetDefinitionToFile(storageSetDef, false);

    }

    @Override
    public void removeContentPoolDefinition(String storageName, ContentPoolDef contentPoolDef) throws ConfigurationHandlerException {

        StorageDef storageDef = getStorageDefinitionFromName(storageName);

        storageDef.getContentPoolDefs().remove(contentPoolDef);

        StorageSetDef storageSetDef = storageDef.getStorageSetDef();

        writeStorageSetDefinitionToFile(storageSetDef, false);
    }

    @Override
    public List<String> listAvailablePoolImplmentations() {

        String[] list = AppContext.getApplicationContext().getBeanNamesForType(ContentPoolHandler.class);

        return Arrays.asList(list);
    }

    /**
     * This method only writes to file
     * IT DOEESNT update internal registry!!
     *
     * @param newStorageSetDef
     * @param asNew
     * @return
     * @throws ConfigurationHandlerException
     */
    public boolean writeStorageSetDefinitionToFile(StorageSetDef newStorageSetDef, boolean asNew) throws ConfigurationHandlerException {

        // we assume this storage set is completely new
        if (asNew) {

            if (storageSetDefRegistry.containsKey(newStorageSetDef)) {

                String errMessage = "StorageSet with this name already exists! Name: " + newStorageSetDef;
                logger.error(errMessage);
                throw new ConfigurationHandlerException(errMessage);
            }
        }

        Writer w = null;
        File resourceFile = null;

        try {

            XStream xStream = getXStream();

            resourceFile = new File(calculateConfigPath(newStorageSetDef.getName()));

            w = new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(resourceFile)), XML_ENCODING);
            w.write("<?xml version=\"1.0\" encoding=\"" + XML_ENCODING + "\"?>\n");
            xStream.toXML(newStorageSetDef, w);

        } catch (FileNotFoundException e) {

            String errMessage = "Error occurred while opening file: " + resourceFile;
            logger.error(errMessage, e);
            throw new ConfigurationHandlerException(errMessage, e);

        } catch (UnsupportedEncodingException e) {

            String errMessage = "Unsupported encoding: " + XML_ENCODING;
            logger.error(errMessage, e);
            throw new ConfigurationHandlerException(errMessage, e);

        } catch (IOException e) {

            String errMessage = "Error occured while working with storage configuration resource stream.";
            logger.error(errMessage, e);
            throw new ConfigurationHandlerException(errMessage, e);

        } finally {

            // close stream to finalize XML
            utilHelper.closeOutputStreamWriter(w);
        }

        return true;
    }

    private String calculateConfigPath(String storageSetName) {

        return storageConfigurationDirectory + File.separator + storageSetName + "." + CONF_EXTENSIONS[0];
    }

}
